package ca.etsmtl.log730.projet2.connectors.clientserver;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.mina.core.session.IoSession;

import ca.etsmtl.log730.projet2.connectors.clientserver.nodes.DocumentMap;
import ca.etsmtl.log730.projet2.connectors.clientserver.nodes.NodeEntry;
import ca.etsmtl.log730.projet2.connectors.clientserver.nodes.SessionNodeMap;
import ca.etsmtl.log730.projet2.connectors.msg.Confirmation;
import ca.etsmtl.log730.projet2.connectors.msg.Request;
import ca.etsmtl.log730.projet2.connectors.msg.RequestReply;
import ca.etsmtl.log730.projet2.connectors.msg.document.AccessCMD;
import ca.etsmtl.log730.projet2.connectors.msg.document.AccessCMD.Action;
import ca.etsmtl.log730.projet2.connectors.msg.document.AccessCMD.Mode;
import ca.etsmtl.log730.projet2.connectors.msg.document.DocumentCMD;
import ca.etsmtl.log730.projet2.documents.Document;

public class RequestManager implements FileManipulator {

	private SessionNodeMap sessionNodeMap;
	private DocumentMap documentMap;
	public DocumentMap getDocumentMap() {
		return documentMap;
	}

	public void setDocumentMap(DocumentMap documentMap) {
		this.documentMap = documentMap;
	}

	public RequestManager(SessionNodeMap snm, DocumentMap dm) {
		sessionNodeMap = snm;
		documentMap = dm;
		counter.set(0);
	}
	private Set<Request> history = new TreeSet<Request>();
	private final AtomicInteger counter = new AtomicInteger();
	private final AtomicInteger lastExec = new AtomicInteger(0);
	
	public AtomicInteger getCounter() {
		return counter;
	}
	private Map<UUID, RequestStatus> currentRequest = new HashMap<UUID, RequestStatus>();
	private Set<Request> pending = new TreeSet<Request>();
	
	public void sendRequest(final Request r) {
		new Thread(new Runnable() {

			@Override
			public void run() {
				r.setOrdinal(counter.get());
				counter.incrementAndGet();
				System.out.println("NEW REQUEST!!! [" + r.getOrdinal() + "]");
				
				history.add(r);
				currentRequest.put(r.getId(), new RequestStatus(r));
				if(sessionNodeMap.size() != 0) {
					
					for(Entry<IoSession, NodeEntry> m: sessionNodeMap.entrySet()) {
						m.getKey().write(r);
					}
					Set<RequestListener> set = null;
					if((set = requestListenerMap.get(r)) != null) {
						for(RequestListener rl: set) {
							rl.requestSent(RequestManager.this, r);
						}
					}
				} else {
					final Confirmation cc = new Confirmation.Apply(r);
					
					currentRequest.remove(r.getId());
					applyConfirmation(cc);
				}
			}
			
		}).start();

	}
	
	public void addReply(Request r, NodeEntry ne, RequestReply rr) {
		RequestStatus rw = currentRequest.get(r.getId());
		if(rr instanceof RequestReply.Accepted) {
			rw.addReply(ne);
		} else {
			rw.setCancelled(true);
		}
		if(rw.isReady()) {
			Confirmation oo = null;
			if(!rw.isCancelled()) {
				oo = new Confirmation.Apply(r);
			} else {
				oo = new Confirmation.Cancelled(r);
			}
			final Confirmation cc = oo;
			currentRequest.remove(r.getId());
			applyConfirmation(cc);
			new Thread(new Runnable() {

				@Override
				public void run() {
					for(Entry<IoSession, NodeEntry> m: sessionNodeMap.entrySet()) {
						m.getKey().write(cc);
					}
				}
				
			}).start();
		}
	}
	public void handleRequest(final IoSession ne, final Request r) {
		pending.add(r);
		counter.set(1+Math.max(counter.get(),r.getOrdinal()));
		new Thread(new Runnable() {

			@Override
			public void run() {
				RequestReply rr = r.createRequestReply(RequestManager.this);
				ne.write(rr);
			}
		}).start();
	}
	public void applyConfirmation(final Confirmation c) {
		new Thread(new Runnable() {

			@Override
			public void run() {
				while((lastExec.get() - c.getRequest().getOrdinal()) > 1) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				lastExec.set(c.getRequest().getOrdinal());
				if(c instanceof Confirmation.Apply) {
					c.getRequest().apply(documentMap);
				}
				Set<RequestListener> set = null;
				if((set = requestListenerMap.get(c.getRequest())) != null) {
					for(RequestListener rl: set) {
						rl.requestConfirmed(RequestManager.this, c.getRequest(), c instanceof Confirmation.Apply);
					}
				}
				pending.remove(c.getRequest());
				history.add(c.getRequest());
				System.out.println("CONFIRMATION " + c.getRequest().getId());
			}
			
		}).start();
	}
	
	
	public class RequestStatus {
		private List<NodeEntry> replyList = new ArrayList<NodeEntry>();
		private RequestStatus (Request req) {
			
			replyList.addAll(sessionNodeMap.values());
		}
		private boolean cancelled = false;
		public void setCancelled(boolean b) {
			cancelled = true;
		}
		public boolean isCancelled() {
			return cancelled;
		}
		public void addReply(NodeEntry ne) {
			replyList.remove(ne);
		}
		
		public int getRemaining() {
			return replyList.size();
		}
		
		public boolean isReady() {
			return cancelled || replyList.size() == 0;
		}
	}


	@Override
	public Request openView(UUID id) {
		if(!documentMap.getViewers(id).contains(sessionNodeMap.getCurrentNode().getUser())) {
			Request r = new AccessCMD(id, sessionNodeMap.getCurrentNode().getUser(), Action.ADD, Mode.VIEW);
			sendRequest(r);
			return r;
		}
		return null;
	}

	@Override
	public Request closeView(UUID id) {
		Request r = new AccessCMD(id, sessionNodeMap.getCurrentNode().getUser(), Action.REMOVE, Mode.VIEW);
		sendRequest(r);
		return r;
	}

	@Override
	public Request updateDocument(UUID id, Document nd) {
//		if(this.documentMap.get(id).getAccess().getEditors().contains(sessionNodeMap.getCurrentNode().getUser())) {
			Request r = new DocumentCMD(DocumentCMD.ModType.CHANGE, id, nd, sessionNodeMap.getCurrentNode().getUser());
			sendRequest(r);
			return r;
//		}
//		return null;
	}

	@Override
	public Request openEdit(UUID id) {
		if(!documentMap.getEditorQueue(id).contains(sessionNodeMap.getCurrentNode().getUser())) {
			Request r = new AccessCMD(id, sessionNodeMap.getCurrentNode().getUser(), Action.ADD, Mode.EDIT);
			sendRequest(r);
			return r;
		}
		Request r = new AccessCMD(id, sessionNodeMap.getCurrentNode().getUser(), Action.ADD, Mode.EDIT);
		sendRequest(r);
		return r;
	}

	@Override
	public Request closeEdit(UUID id) {
		Request r = new AccessCMD(id, sessionNodeMap.getCurrentNode().getUser(), Action.REMOVE, Mode.EDIT);
		sendRequest(r);
		return r;
	}

	@Override
	public Request deleteDocument(UUID id) {
		if(documentMap.getViewers(id).size() == 0 && documentMap.getEditorQueue(id).size() == 0) {
			Request r = new DocumentCMD(DocumentCMD.ModType.REMOVE, id, null, sessionNodeMap.getCurrentNode().getUser());
			sendRequest(r);
			return r;
		}
		
		return null;
	}

	@Override
	public Request insertDocument(UUID id, Document nd) {
		Request r = new DocumentCMD(DocumentCMD.ModType.ADD, id, nd, sessionNodeMap.getCurrentNode().getUser());
		sendRequest(r);
		return r;
	}
	
	private Map<Request, Set<RequestListener>> requestListenerMap = new TreeMap<Request, Set<RequestListener>>();
	
	public void register(RequestListener rl, Request r) {
		if(!requestListenerMap.containsKey(r)) {
			requestListenerMap.put(r, new HashSet<RequestListener>());
		}
		requestListenerMap.get(r).add(rl);
	}
	
	public void unregister(RequestListener rl, Request r) {
		if(requestListenerMap.containsKey(r)) {
			requestListenerMap.get(r).remove(rl);
		}
	}

	public static interface RequestListener {
		public void requestSent(RequestManager rm, Request r);
		public void requestReplyReceived(RequestManager rm, Request r, float t);
		public void requestConfirmed(RequestManager rm, Request r, boolean accepted);
		public void requestDisposed(RequestManager rm, Request r);
	}
}
